package org.chench.extra.logsift.service.impl;

import cn.hutool.core.util.StrUtil;
import io.krakens.grok.api.Grok;
import io.krakens.grok.api.GrokCompiler;
import io.krakens.grok.api.Match;
import org.apache.commons.lang3.StringUtils;
import org.chench.extra.logsift.bo.LogApp;
import org.chench.extra.logsift.bo.PathPattern;
import org.chench.extra.logsift.io.CodifiableRandomAccessFile;
import org.chench.extra.logsift.service.FileMonitor;
import org.chench.extra.logsift.service.LogHandler;
import org.chench.extra.logsift.service.LogMonitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 日志监听器实现
 * @author chench
 * @date 2025.05.10
 */
public class LogMonitorImpl implements LogMonitor {
    private static final Logger logger = LoggerFactory.getLogger(LogMonitorImpl.class);
    private static final String GROK_PATTERN_DEFAULT = "%{CUSTOM_TIMESTAMP:timestamp} \\[%{DATA:thread}\\] %{LOGLEVEL:level}%{SPACE}{2}%{DATA:logger} - %{GREEDYDATA:message}";

    // 运行标记
    private final AtomicBoolean running = new AtomicBoolean(true);

    // 文件监听器集合
    private final Map<Path, FileMonitor> fileMonitors = new ConcurrentHashMap<>();
    // 文件指针集合
    private final Map<Path, Long> filePointers = new ConcurrentHashMap<>();
    // 目录匹配模式集合
    private final Map<Path, List<Pattern>> directoryPatterns = new ConcurrentHashMap<>();
    // 文件指针保存定时器
    private final ScheduledExecutorService filePointerScheduler = Executors.newScheduledThreadPool(1);
    // 文件处理器
    private final ExecutorService fileProcessor = Executors.newFixedThreadPool(5);

    // 日志处理器上下文
    private LogHandler logHandlerContext;
    // 指针文件路径，用于保存文件指针
    private final Path pointerFilePath;
    // 日志应用集合
    private final Map<String, LogApp> logAppMap;

    public LogMonitorImpl(Path pointerFilePath, Map<String, LogApp> logAppMap) {
        this.pointerFilePath = pointerFilePath;
        this.logAppMap = logAppMap;
    }

    @Override
    public void startup() throws IOException {
        if (this.logAppMap == null || this.logAppMap.isEmpty()) {
            logger.warn("logApp config is empty!");
            return;
        }

        for (Map.Entry<String, LogApp> entry : this.logAppMap.entrySet()) {
            String logAppName = entry.getKey();
            LogApp logApp = entry.getValue();

            // 初始化Grok模式，匹配日志格式
            GrokCompiler compiler = GrokCompiler.newInstance();
            compiler.registerDefaultPatterns();

            // 自定义TIMESTAMP模式
            if (!StrUtil.isEmpty(logApp.getTimestampPattern())) {
                compiler.register("CUSTOM_TIMESTAMP", logApp.getTimestampPattern());
            }

            // 完整的Grok模式
            String contentPattern = logApp.getContentPattern();
            if (StrUtil.isEmpty(contentPattern)) {
                contentPattern = GROK_PATTERN_DEFAULT;
            }
            Grok grok = compiler.compile(contentPattern);

            // 解析路径模式
            addPathPattern(logAppName, logApp.getFilterLevel(), logApp.getPathPattern(), grok);
        }

        // 加载已保存的文件指针
        loadFilePointers();

        // 定期保存文件指针
        this.filePointerScheduler.scheduleAtFixedRate(this::saveFilePointers, 10, 10, TimeUnit.SECONDS);
    }

    @Override
    public void shutdown() {
        logger.info("开始关闭日志监听器...");
        this.running.set(false);
        this.filePointerScheduler.shutdown();
        this.fileProcessor.shutdown();

        try {
            if (!this.filePointerScheduler.awaitTermination(5, TimeUnit.SECONDS)) {
                this.filePointerScheduler.shutdownNow();
            }
            if (!this.fileProcessor.awaitTermination(5, TimeUnit.SECONDS)) {
                this.fileProcessor.shutdownNow();
            }
        } catch (InterruptedException e) {
            logger.error("execute shutdown error: {}", e.getMessage(), e);
            this.filePointerScheduler.shutdownNow();
            this.fileProcessor.shutdownNow();
        }

        saveFilePointers();
        logger.info("日志监听器已关闭！");
    }

    @Override
    public void setLogHandler(LogHandler logHandler) {
        this.logHandlerContext = logHandler;
    }

    /**
     * 添加路径模式
     * @param logAppName 采集日志应用名
     * @param pathPattern 采集日志路径模式
     * @param grok 日志内容解析
     */
    private void addPathPattern(String logAppName, String logLevel, String pathPattern, Grok grok) {
        try {
            // 解析带通配符的路径
            PathPattern pattern = parsePathPattern(pathPattern);
            if (pattern != null) {
                watchingDirectoryWithPattern(logAppName, logLevel, pattern.getPath(), pattern.getPatterns(), grok);
            }
        } catch (Exception e) {
            logger.error("无效的路径模式[{}]: {}", pathPattern, e.getMessage(), e);
        }
    }

    /**
     * 解析带通配符的路径
     * @param path 解析路径模式
     * @return {@link PathPattern}
     */
    private PathPattern parsePathPattern(String path) {
        try {
            // 分离目录部分和文件名部分
            int lastSeparator = path.lastIndexOf(File.separatorChar);
            String dirPart = (lastSeparator >= 0) ? path.substring(0, lastSeparator) : ".";
            String filePart = (lastSeparator >= 0) ? path.substring(lastSeparator + 1) : path;

            // 创建目录路径对象
            Path dirPath = Paths.get(dirPart);

            // 检查目录是否存在
            if (Files.notExists(dirPath)) {
                logger.error("目录不存在:{}", dirPath);
                return null;
            }

            // 将通配符转换为正则表达式
            String regex = filePart
                    .replace(".", "\\.")
                    .replace("*", ".*")
                    .replace("?", ".");

            List<Pattern> patterns = Collections.singletonList(Pattern.compile(regex));
            return new PathPattern(dirPath, patterns);
        } catch (Exception e) {
            logger.error("解析路径模式失败[{}]: {}", path, e.getMessage(), e);
            return null;
        }
    }

    /**
     * 监控带模式的目录
     * @param logAppName 采集日志应用名
     * @param path 路径
     * @param patterns 模式
     * @param grok 日志内容解析
     */
    private void watchingDirectoryWithPattern(String logAppName, String logLevel, Path path, List<Pattern> patterns, Grok grok) {
        // 保存目录对应的文件匹配模式
        directoryPatterns.computeIfAbsent(path, k -> new CopyOnWriteArrayList<>()).addAll(patterns);
        logger.info("开始监控目录 [{}] 中的文件: [{}]", path,
                patterns.stream().map(Pattern::pattern).collect(Collectors.joining(" | ")));

        try {
            // 扫描目录下符合模式的现有文件
            Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path filePath, BasicFileAttributes attributes) {
                    if (isFileMatchPatterns(filePath, patterns)) {
                        watchingFile(logAppName, logLevel, filePath, grok);
                    }
                    return FileVisitResult.CONTINUE;
                }
            });

            // 注册目录监听器
            WatchService watchService = FileSystems.getDefault().newWatchService();
            path.register(watchService,
                    // 目录中新建文件
                    StandardWatchEventKinds.ENTRY_CREATE,
                    // 目录中的文件被修改
                    StandardWatchEventKinds.ENTRY_MODIFY
            );

            // 启动目录监控线程
            String threadName = "DirectoryWatcher-" + path.getFileName();
            new Thread(() -> {
                try {
                    while (running.get()) {
                        WatchKey key = watchService.take();
                        for (WatchEvent<?> event : key.pollEvents()) {
                            Path changed = path.resolve((Path) event.context());
                            if (!Files.isRegularFile(changed)) {
                                continue;
                            }
                            List<Pattern> dirPatterns = directoryPatterns.get(path);
                            if (dirPatterns != null && isFileMatchPatterns(changed, dirPatterns)) {
                                if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
                                    watchingFile(logAppName, logLevel, changed, grok);
                                } else if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
                                    processFileUpdate(changed);
                                }
                            }
                        }
                        key.reset();
                    }
                } catch (Exception e) {
                    if (running.get()) {
                        logger.error("目录监控错误[{}]: {}", path, e.getMessage(), e);
                    }
                }
            }, threadName).start();
        } catch (Exception e) {
            logger.info("无法监控目录[{}]: {}", path, e.getMessage(), e);
        }
    }

    /**
     * 检查文件是否匹配任何模式
     * @param filePath 文件路径
     * @param patterns 匹配模式列表
     * @return true：文件匹配模式成功，false：文件未匹配任何模式
     */
    private boolean isFileMatchPatterns(Path filePath, List<Pattern> patterns) {
        String fileName = filePath.getFileName().toString();
        return patterns.stream().anyMatch(p -> p.matcher(fileName).matches());
    }

    /**
     * 监控单个文件
     * @param logAppName 日志采集应用名
     * @param logLevel 需要过滤的日志级别
     * @param filePath 日志文件路径
     * @param grok 日志内容解析
     */
    private void watchingFile(String logAppName, String logLevel, Path filePath, Grok grok) {
        if (fileMonitors.containsKey(filePath)) {
            return;
        }

        try {
            // 初始化文件指针
            if (!filePointers.containsKey(filePath)) {
                filePointers.put(filePath, Files.size(filePath));
            }

            // 创建文件监控器
            FileMonitorImpl monitor = new FileMonitorImpl(logAppName, logLevel, filePath, grok);
            fileMonitors.put(filePath, monitor);

            logger.info("开始监控文件: {}", filePath);
        } catch (Exception e) {
            logger.error("无法监控文件 [{}]: {}", filePath, e.getMessage(), e);
        }
    }

    /**
     * 处理文件更新
     * @param filePath 文件路径
     */
    private void processFileUpdate(Path filePath) {
        FileMonitor monitor = this.fileMonitors.get(filePath);
        if (monitor != null) {
            this.fileProcessor.submit(monitor::checkForUpdates);
        }
    }

    /**
     * 加载已保存的文件指针
     */
    private void loadFilePointers() throws IOException {
        if (!Files.exists(this.pointerFilePath)) {
            logger.warn("pointer file not exists！do create: {}", this.pointerFilePath);
            // 判断当前系统是否支持POSIX文件系统：使用当前工作目录（必然存在）来获取文件存储
            boolean isPosixSupported = Files.getFileStore(Paths.get(".")).supportsFileAttributeView("posix");
            if (isPosixSupported) {
                // 类Unix系统：设置POSIX权限（例如"rw-r--r--"）
                Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-r--r--");
                FileAttribute<?> attr = PosixFilePermissions.asFileAttribute(perms);
                Files.createFile(this.pointerFilePath, attr);
            } else {
                // Windows系统：不设置POSIX权限，直接创建
                Files.createFile(this.pointerFilePath);
            }
        }

        try (BufferedReader reader = Files.newBufferedReader(this.pointerFilePath)) {
            String line;
            while ((line = reader.readLine()) != null) {
                String[] parts = line.split("\\|", 2);
                if (parts.length == 2) {
                    try {
                        Path filePath = Paths.get(parts[0]);
                        long pointer = Long.parseLong(parts[1]);
                        this.filePointers.put(filePath, pointer);
                    } catch (Exception e) {
                        logger.error("解析文件指针失败: {}", line, e);
                    }
                }
            }
            logger.info("已加载 {} 个文件的指针位置", this.filePointers.size());
        } catch (IOException e) {
            logger.error("加载指针文件失败: {}", e.getMessage(), e);
        }
    }

    /**
     * 保存文件指针
     */
    private void saveFilePointers() {
        try (BufferedWriter writer = Files.newBufferedWriter(this.pointerFilePath)) {
            for (Map.Entry<Path, Long> entry : this.filePointers.entrySet()) {
                writer.write(entry.getKey() + "|" + entry.getValue() + "\n");
            }
        } catch (IOException e) {
            logger.error("保存文件指针失败: {}", e.getMessage(), e);
        }
    }

    // 文件监听器实现类
    private class FileMonitorImpl implements FileMonitor {
        // 日志采集应用名
        private final String logAppName;
        // 需要过滤的日志级别
        private final String logLevel;
        // 日志文件路径
        private final Path filePath;
        // 日志内容解析器
        private final Grok grok;
        private long lastModified = 0;

        public FileMonitorImpl(String logAppName, String logLevel, Path filePath, Grok grok) {
            this.logAppName = logAppName;
            this.logLevel = logLevel;
            this.filePath = filePath;
            this.grok = grok;
            try {
                this.lastModified = Files.getLastModifiedTime(filePath).toMillis();
            } catch (IOException e) {
                logger.error("获取文件修改时间失败 [{}]: {}", filePath, e.getMessage(), e);
            }
        }

        @Override
        public void checkForUpdates() {
            try {
                // 检查文件是否存在
                if (Files.notExists(this.filePath)) {
                    fileMonitors.remove(this.filePath);
                    filePointers.remove(this.filePath);
                    logger.error("文件已删除: {}", this.filePath);
                    return;
                }

                // 检查文件是否被修改
                long currentModified = Files.getLastModifiedTime(this.filePath).toMillis();
                if (currentModified <= this.lastModified) {
                    return;
                }
                this.lastModified = currentModified;

                // 获取当前文件大小
                long fileSize = Files.size(this.filePath);
                long position = filePointers.getOrDefault(this.filePath, 0L);

                // 处理文件截断情况
                if (fileSize < position) {
                    position = 0;
                }

                // 如果有新增内容，处理之
                if (fileSize > position) {
                    processNewLines(position);
                    filePointers.put(filePath, fileSize);
                }
            } catch (Exception e) {
                logger.error("检查文件更新失败 [{}]: {}", filePath, e.getMessage(), e);
            }
        }

        private void processNewLines(long startPosition) {
            // 需要监控的日志级别
            String targetLevel = this.logLevel;
            try (CodifiableRandomAccessFile file = new CodifiableRandomAccessFile(this.filePath.toFile(), "r")) {
                file.seek(startPosition);

                // 将多行异常堆栈读取为一行
                StringBuilder currentLog = new StringBuilder();
                String line;
                boolean isStackTrace = false;

                while ((line = file.readLineWithEncoding(StandardCharsets.UTF_8.name())) != null) {
                    // 判断是否为堆栈信息的开始（以\tat开头）
                    if (line.trim().startsWith("at ") || line.trim().startsWith("Caused by: ")) {
                        isStackTrace = true;
                        currentLog.append("\n").append(line);
                        continue;
                    }

                    // 如果当前不是堆栈信息，且之前有堆栈信息，则处理之前的日志
                    if (!isStackTrace || line.trim().isEmpty()) {
                        if (currentLog.length() > 0) {
                            processLogLine(currentLog.toString(), targetLevel);
                            currentLog.setLength(0);
                        }
                        isStackTrace = false;
                    }

                    // 开始新的日志行
                    if (!line.trim().isEmpty()) {
                        currentLog.append(line);
                    }
                }

                // 处理最后一条日志
                if (currentLog.length() > 0) {
                    processLogLine(currentLog.toString(), targetLevel);
                }
            } catch (Exception e) {
                logger.error("处理文件新增内容失败 [{}]: {}", filePath, e.getMessage(), e);
            }
        }

        private void processLogLine(String line, String targetLevel) {
            if (StringUtils.isBlank(targetLevel)) {
                // 监控所有日志
                logHandlerContext.handleLog(logAppName, this.filePath, new HashMap<>(0), line);
                return;
            }

            Match matcher = this.grok.match(line);
            if (matcher.getMatch() == null) {
                // logger.error("日志格式模式匹配结果为空，filePath: {}, line: {}", this.filePath, line);
                logHandlerContext.handleLog(logAppName, this.filePath, null, line);
            } else if (matcher.getMatch().matches()) {
                Map<String, Object> logData = matcher.capture();
                if (targetLevel.equals(logData.get("level"))) {
                    // 监控指定级别日志
                    logHandlerContext.handleLog(logAppName, this.filePath, logData, line);
                }
            } else {
                //logger.error("日志格式与监控模式不匹配，filePath:{}, line: {}", this.filePath, line);
                logHandlerContext.handleLog(logAppName, this.filePath, null, line);
            }
        }
    }

}